﻿/*
 * This file is part of MonoStrategy.
 *
 * Copyright (C) 2010-2011 Christoph Husse
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: 
 *      # Christoph Husse
 * 
 * Also checkout our homepage: http://monostrategy.codeplex.com/
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

namespace MonoStrategy
{
    /* Correlates with MovableType */
    public enum WallValue
    {
        Free = 0,
        Reserved = 1,
        SettlerUnwalkable = 9,
        SoldierUnwalkable = 10,
        Building = 20,

        WaterBorder = 231,
    }

    [Flags]
    public enum TerrainCellFlags
    {
        None = 0x00,
        
        Tree_01 = 0x01,
        Tree_02 = 0x02,
        Tree_03 = 0x03,
        Tree_04 = 0x04,
        Tree_05 = 0x05,
        TreeMask = 0x07,

        Stone = 0x08,

        Grading = 0x40,
        Snow = 0x80,

        // collects flags that prevents a cell from being used for buildings
        WallMask = TreeMask | Snow | Stone,
    }

    public struct GradingInfo
    {
        internal const Int32 GradingStrength = 3;
        internal int AvgHeight;
        internal int Variance;
    }

    [Serializable]
    public class TerrainDefinition
    {
        private readonly int m_SizeShift;
        private byte[] m_HeightMap;
        private byte[] m_WaterDistMap;
        internal byte[] m_WallMap;
        private byte[] m_FlagMap;
        private readonly byte[] m_BuildingGrid;
        private byte[] m_GroundMap;
        private readonly CrossRandom m_Random = new CrossRandom(1);
        private readonly int m_WaterHeight;
        private List<Point> m_Spots = new List<Point>();

        [NonSerialized]
        private BuildingConfig m_BuildingConfig;
        [NonSerialized]
        private GameMap m_Map;

        public BuildingConfig BuildingConfig { get { return m_BuildingConfig; } }
        public IEnumerable<Point> Spots { get { return m_Spots.AsReadOnly(); } }
        public XMLTerrainConfig Config { get; private set; }
        public int Size { get; private set; }
        public GameMap Map { get { return m_Map; } }
        [field: NonSerialized]
        public event Procedure<Point> OnCellChanged;

        public TerrainDefinition(GameMap inMap, XMLTerrainConfig inConfig)
            : this(inMap.Size, inConfig)
        {
            m_Map = inMap;
        }

        public TerrainDefinition(int inSize, XMLTerrainConfig inConfig)
        {
            if (inConfig == null)
                throw new ArgumentNullException();

            ValidateConfig(inConfig);

            Config = inConfig;
            Size = inSize;
            m_SizeShift = (int)Math.Floor(Math.Log((double)Size, 2) + 0.5);

            if ((int)Math.Pow(2, (double)m_SizeShift) != Size)
                throw new ApplicationException("Grid size must be a power of two.");

            m_HeightMap = new byte[Size * Size];
            m_WallMap = new byte[Size * Size];
            m_WaterDistMap = new byte[Size * Size];
            m_FlagMap = new byte[Size * Size];
            m_BuildingGrid = new byte[Size * Size];
            m_GroundMap = new byte[Size * Size];

            m_WaterHeight = (int)((1.0 + Config.Water.Height) * 128);

            for (int i = 0; i < m_HeightMap.Length; i++)
            {
                m_HeightMap[i] = 128;
            }
        }

        public void SaveToStream(Stream inStream)
        {
            BinaryFormatter format = new BinaryFormatter();

            format.Serialize(inStream, this);
        }

        public void LoadFromStream(Stream inStream)
        {
            BinaryFormatter format = new BinaryFormatter();
            TerrainDefinition source = (TerrainDefinition)format.Deserialize(inStream);

            if ((source.Size != Size) || (source.m_WaterHeight != m_WaterHeight))
                throw new ArgumentException("The terrain data stored in the stream is not compatible with this instance.");

            this.m_FlagMap = source.m_FlagMap;
            this.m_GroundMap = source.m_GroundMap;
            this.m_HeightMap = source.m_HeightMap;
            this.m_Spots = source.m_Spots;
            this.m_WallMap = source.m_WallMap;
            this.m_WaterDistMap = source.m_WaterDistMap;
        }

        private void ValidateConfig(XMLTerrainConfig inConfig)
        {
            if ((inConfig.BlueNoiseFrequency == 0) || (inConfig.RedNoiseFrequency == 0) || (inConfig.GreenNoiseFrequency == 0))
                throw new ArgumentOutOfRangeException();

            if ((inConfig.Water.Height < -1.0) || (inConfig.Water.Height > 1.0))
                throw new ArgumentOutOfRangeException();

            if (inConfig.Water.Speed == 0)
                throw new ArgumentOutOfRangeException();

            if (inConfig.Levels.Count != 6)
                throw new ArgumentOutOfRangeException();

            foreach (var level in inConfig.Levels)
            {
                if ((level.RedNoiseDivisor == 0) || (level.GreenNoiseDivisor == 0) || (level.BlueNoiseDivisor == 0) || (level.TextureScale == 0))
                    throw new ArgumentOutOfRangeException();
            }
        }

        internal Byte[] GetWallMap()
        {
            return m_WallMap;
        }

        internal bool AddSpot(Point inNewSpot)
        {
            foreach (var spot in m_Spots)
            {
                if (spot.DistanceTo(inNewSpot) < 20)
                    return false; // spots need to be a reasonable distance apart, otherwise it just makes no sense...
            }

            m_Spots.Add(inNewSpot);

            return true;
        }

        private bool IsValidCell(int inXCell, int inYCell)
        {
            if ((inXCell < 0) || (inXCell >= Size))
                return false;

            if ((inYCell < 0) || (inYCell >= Size))
                return false;

            return true;
        }

        private void CheckBounds(int inXCell, int inYCell)
        {
#if DEBUG
            if (!IsValidCell(inXCell, inYCell))
                throw new ArgumentOutOfRangeException();
#endif
        }

        public byte GetGroundAt(int inXCell, int inYCell)
        {
            CheckBounds(inXCell, inYCell);

            return m_GroundMap[inXCell + (inYCell << m_SizeShift)];
        }

        internal void SetGroundAt(int inXCell, int inYCell, byte inGroundType)
        {
            CheckBounds(inXCell, inYCell);

            m_GroundMap[inXCell + (inYCell << m_SizeShift)] = inGroundType;

            if (OnCellChanged != null)
                OnCellChanged(new Point(inXCell, inYCell));
        }

        public byte GetHeightAt(int inXCell, int inYCell)
        {
            CheckBounds(inXCell, inYCell);

            return m_HeightMap[inXCell + (inYCell << m_SizeShift)];
        }

        internal void SetHeightAt(int inXCell, int inYCell, byte inHeight)
        {
            CheckBounds(inXCell, inYCell);

            m_HeightMap[inXCell + (inYCell << m_SizeShift)] = inHeight;

            if (OnCellChanged != null)
                OnCellChanged(new Point(inXCell, inYCell));
        }

        public WallValue GetWallAt(int inXCell, int inYCell)
        {
            CheckBounds(inXCell, inYCell);

            return (WallValue)m_WallMap[inXCell + (inYCell << m_SizeShift)];
        }

        internal void SetWallAt(int inXCell, int inYCell, WallValue inWall)
        {
            CheckBounds(inXCell, inYCell);

            m_WallMap[inXCell + (inYCell << m_SizeShift)] = (byte)inWall;
        }

        internal void SetWallAt(Point inCell, WallValue inWall, params Rectangle[] inAreas)
        {
            foreach (var rect in inAreas)
            {
                for (int x = inCell.X + rect.X; x < inCell.X + rect.Width + rect.X; x++)
                {
                    for (int y = inCell.Y + rect.Y; y < inCell.Y + rect.Height + rect.Y; y++)
                    {
                        SetWallAt(x, y, inWall);
                    }
                }
            }
        }

        /// <summary>
        /// Only sets the wall if the target cells are free.
        /// </summary>
        internal void InitializeWallAt(Point inCell, WallValue inWall, params Rectangle[] inAreas)
        {
            foreach (var rect in inAreas)
            {
                for (int x = inCell.X + rect.X; x < inCell.X + rect.Width + rect.X; x++)
                {
                    for (int y = inCell.Y + rect.Y; y < inCell.Y + rect.Height + rect.Y; y++)
                    {
                        if ((x < 0) || (x >= Size) | (y < 0) || (y >= Size))
                            continue;

                        if (GetWallAt(x, y) == WallValue.Free)
                            SetWallAt(x, y, inWall);
                    }
                }
            }
        }

        public TerrainCellFlags GetFlagsAt(int inXCell, int inYCell)
        {
            CheckBounds(inXCell, inYCell);

            return (TerrainCellFlags)m_FlagMap[inXCell + (inYCell << m_SizeShift)];
        }

        internal void SetFlagsAt(int inXCell, int inYCell, TerrainCellFlags inFlag)
        {
            CheckBounds(inXCell, inYCell);

            m_FlagMap[inXCell + (inYCell << m_SizeShift)] = (byte)inFlag;

            if (OnCellChanged != null)
                OnCellChanged(new Point(inXCell, inYCell));
        }

        public virtual float GetZShiftAt(int inXCell, int inYCell)
        {
            return (float)((-1.0 + GetHeightAt(inXCell, inYCell) / 128.0) * Config.HeightScale);
        }

        public void ResetBuildingGrid(BuildingConfig inConfig)
        {
            if (inConfig == null)
                throw new ArgumentNullException();

            m_BuildingConfig = inConfig;

            Array.Clear(m_BuildingGrid, 0, m_BuildingGrid.Length);
        }

        public GradingInfo GetGradingInfo(int inXCell, int inYCell, BuildingConfig config)
        {
            // determine avergae height on building ground
            GradingInfo result = new GradingInfo();
            int count = 0;
            int avgHeight = 0;

            for (int y = inYCell; y < config.GridHeight + inYCell; y++)
            {
                for (int x = inXCell; x < config.GridWidth + inXCell; x++)
                {
                    count++;
                    avgHeight += GetHeightAt(x, y);
                }
            }

            avgHeight /= count;

            // determine grading effort
            int variance = 0;

            for (int y = inYCell; y < config.GridHeight + inYCell; y++)
            {
                for (int x = inXCell; x < config.GridWidth + inXCell; x++)
                {
                    variance += Math.Abs(avgHeight - GetHeightAt(x, y));
                }
            }

            result.AvgHeight = avgHeight;
            result.Variance = variance;

            return result;
        }

        public virtual bool CanFoilageBePlacedAt(int inXCell, int inYCell, FoilageType inFoilageType)
        {
            for(int x = inXCell - 1; x < inXCell + 1; x++)
            {
                for (int y = inYCell - 1; y < inYCell + 1; y++)
                {
                    if ((x < 0) || (y < 0) || (x >= Size) || (y >= Size))
                        return false;

                    if ((GetWallAt(x, y) != WallValue.Free) || IsWater(x, y))
                        return false;
                }
            }

            for (int x = inXCell - 2; x < inXCell + 2; x++)
            {
                for (int y = inYCell - 2; y < inYCell + 2; y++)
                {
                    if ((x < 0) || (y < 0) || (x >= Size) || (y >= Size))
                        return false;

                    if (GetWallAt(x, y) > WallValue.Reserved)
                        return false;
                }
            }

            return true;
        }

        internal bool FindWater(Point inAround, int inWaterDepth, out Point outWaterSpot)
        {
            Direction unused;

            return FindWater(inAround, inWaterDepth, out outWaterSpot, out unused);
        }

        internal bool IsWater(Point inPosition)
        {
            return IsWater(inPosition.X, inPosition.Y);
        }

        internal bool IsWater(int inX, int inY)
        {
            return (GetHeightAt(inX, inY) < m_WaterHeight);
        }

        internal bool FindWater(Point inAround, int inWaterDepth, out Point outWaterSpot, out Direction outWaterDir)
        {
            Point waterSpot = new Point(0,0);
            Direction waterDir = Direction._045;

            outWaterSpot = waterSpot;
            outWaterDir = waterDir;

            if (GetHeightAt(inAround.X, inAround.Y) < m_WaterHeight)
                throw new InvalidOperationException("Can only query for water from land.");

            if(WalkResult.Success != GridSearch.GridWalkAround(inAround, Size, Size, (pos) =>
                    {
                        int height = GetHeightAt(pos.X, pos.Y);

                        if (height >= m_WaterHeight)
                            return WalkResult.NotFound;

                        // walk back towards building and count water cells
                        double dirX = pos.X - inAround.X;
                        double dirY = pos.Y - inAround.Y;
                        double normFactor = Math.Sqrt(dirX * dirX + dirY * dirY);

                        dirX /= normFactor;
                        dirY /= normFactor;

                        int waterCount = 1;
                        Point last = pos, current = pos;
                        double currentX = pos.X;
                        double currentY = pos.Y;

                        while ((GetHeightAt(current.X, current.Y) < m_WaterHeight))
                        {
                            if (last != current)
                                waterCount++;

                            currentX -= dirX;
                            currentY -= dirY;
                            last = current;
                            current = new Point((int)currentX, (int)currentY);
                        }

                        if (waterCount != inWaterDepth)
                            return WalkResult.NotFound;

                        waterSpot = current;
                        waterDir = DirectionUtils.GetNearestWalkingDirection(new Point((int)Math.Round(dirX), (int)Math.Round(dirY))).Value;

                        return WalkResult.Success;
                    }))
                return false;

            outWaterSpot = waterSpot;
            outWaterDir = waterDir;

            return true;
        }

        public virtual bool CanBuildingBePlacedAt(int inXCell, int inYCell, BuildingConfig config)
        {
            // check if building can be build here at all
            Boolean isMine = typeof(MineBuilding).IsAssignableFrom(config.ClassType);

            foreach (var pos in config.GroundPlane.Concat(config.ReservedPlane))
            {
                int x = pos.X + inXCell, y = pos.Y + inYCell;

                if ((x < 0) || (y < 0) || (x >= Size) || (y >= Size))
                    return false;

                float layer = (-1.0f + GetHeightAt(x,y) / 128.0f);
                float height = (float)(layer * Config.HeightScale);
                Boolean hasPassed = false;

                do
                {
                    if (GetWallAt(x, y) != WallValue.Free)
                        break;

                    if (layer <= Config.Water.Height)
                        break;

                    var flags = GetFlagsAt(x, y);

                    if ((flags & TerrainCellFlags.WallMask) != 0)
                        break;

                    if (isMine)
                    {

                        if (layer < Config.Levels[3].Margin)
                            break;
                    }
                    else if (layer >= Config.Levels[3].Margin)
                        break;

                    hasPassed = true;
                } while (false);

                if (!hasPassed)
                    return false;
            }

            return true;
        }

        public virtual byte GetBuildingExpenses(int inXCell, int inYCell)
        {
            var config = BuildingConfig;

            if (config == null)
                return 0;

            inXCell -= config.GridWidth / 2;
            inYCell -= (int)((config.GridHeight * 1/Math.Sqrt(2)) / 2);

            if ((inXCell < 0) || (inYCell < 0) || (inXCell + config.GridWidth >= Size) || (inYCell + config.GridHeight >= Size))
                return 0;

            byte result = m_BuildingGrid[inXCell + (inYCell << m_SizeShift)];

            if (result > 0)
                return result; // already cached

            if (!CanBuildingBePlacedAt(inXCell, inYCell, config))
            {
                // not buildable here
                result = 1;
            }
            else
            {
                // determine avergae height on building ground
                double variance = GetGradingInfo(inXCell, inYCell, config).Variance;

                variance = Math.Log(variance / GradingInfo.GradingStrength, 2.5);

                result = (byte)variance;
            }

            m_BuildingGrid[inXCell + (inYCell << m_SizeShift)] = result;

            return result;
        }

        /// <summary>
        /// Changes the height of the given cell as well as its surrounding accordingly to
        /// the given brush. These painting operations are the preferred way of changing
        /// terrain properties, since they cause a more natural look and feel.
        /// </summary>
        /// <param name="inXCell"></param>
        /// <param name="inYCell"></param>
        /// <param name="inBrush"></param>
        internal void DrawToHeightmap(int inXCell, int inYCell, TerrainBrush inBrush)
        {

        }

        /// <summary>
        /// Instead of <see cref="DrawToHeightmap"/>, this one will use the brush to reduce
        /// the terrain variance from average. This will in fact grade the terrain to the
        /// given average height by using the brush as weight for each surrounding cell.
        /// </summary>
        /// <param name="inXCell"></param>
        /// <param name="inYCell"></param>
        /// <param name="inAvgHeight"></param>
        /// <param name="inBrush"></param>
        internal void AverageDrawToHeightmap(int inXCell, int inYCell, int inAvgHeight, TerrainBrush inBrush)
        {
            for (int x = inXCell - inBrush.Width / 2, ix = 0; x < inXCell + inBrush.Width / 2; x++, ix++)
            {
                for (int y = inYCell - inBrush.Height / 2, iy = 0; y < inYCell + inBrush.Height / 2; y++, iy++)
                {
                    if (!IsValidCell(x, y))
                        continue;

                    int height = GetHeightAt(x, y);
                    int diff = inAvgHeight - height;
                    int newHeight = height + Math.Sign(diff) * Math.Abs(inBrush.Values[ix, iy]);

                    if ((newHeight < inAvgHeight) && (height >= inAvgHeight))
                        newHeight = inAvgHeight;
                    else if ((newHeight > inAvgHeight) && (height <= inAvgHeight))
                        newHeight = inAvgHeight;

                    SetHeightAt(x, y, (byte)newHeight);
                }
            }
        }

        internal WalkResult EnumAround(Point inAround, Func<Point, WalkResult> inHandler)
        {
            return GridSearch.GridWalkAround(inAround, Size, Size, (pos) =>
            {
                return inHandler(pos);
            });
        }

    }
}
